using System;
using System.Collections.Generic;
using System.Text;
using TestUtil.Equality;

namespace Crm.Domain.Tests
{
    /// <summary>
    /// Helps with testing the equality behavior of reference types that are value objects (Evans).
    /// </summary>
    /// <typeparam name="TValueObject">A value object that is a reference type. This class can only be used on reference types
    /// because they have an implementation of "==" so the tests can use that implementation.</typeparam>
    public class ReferenceTypeValueObjectEqualityTestHelper<TValueObject> where TValueObject : class
    {
        # region Fields

        private CreateTestObjectCallback<TValueObject> _createTestObjectCallback;
        private CreateTestObjectCallback<TValueObject> _createUnequalTestObjectCallback;

        # endregion

        #region Constructor

        /// <summary>
        /// Creates an instance of this class.
        /// </summary>
        /// <param name="firstCallback">Each time this is called it will create a new object, the objects created by this method
        /// should not be equal to the objects created by <paramref name="secondCallback"/>.</param>
        /// <param name="secondCallback">Each time this is called it will create a new object, the objects created by this method
        /// should not be equal to the objects created by <paramref name="firstCallback"/>.</param>
        public ReferenceTypeValueObjectEqualityTestHelper(CreateTestObjectCallback<TValueObject> firstCallback,
                                                          CreateTestObjectCallback<TValueObject> secondCallback)
        {
            _createTestObjectCallback = firstCallback;
            _createUnequalTestObjectCallback = secondCallback;
        }

        #endregion

        #region Public Methods

        public void PerformTest()
        {
            TestEqualsMethod();
            TestEqualsOperators();

            SymmetricEqualityTest();
            TransitiveEqualityTest();
        } 

        #endregion

        #region Private Methods

        private void TestEqualsMethod()
        {
            TValueObject first = _createTestObjectCallback();
            TValueObject shouldBeEqualToFirst = _createTestObjectCallback();

            ThrowIfFalse(first.Equals(shouldBeEqualToFirst), "The equals method is not identifying when two objects have the same values.");

            TValueObject notEqualToFirst  = _createUnequalTestObjectCallback();

            ThrowIfTrue(first.Equals(notEqualToFirst), "The equals method is not identifying when two objects have the different values.");
        }

        /// <summary>
        /// The equals operators should always use reference type equality semantics.
        /// </summary>
        private void TestEqualsOperators()
        {
            TValueObject first = _createTestObjectCallback();
            TValueObject notEqualToFirst = _createUnequalTestObjectCallback();

            ThrowIfFalse(first == first, "== should identify that an equal itself.");
            ThrowIfTrue(first == notEqualToFirst, "== should identify that objects are only equal if they are the same reference.");

            ThrowIfTrue(first != first, "!= should be false when used to compare an object to itself.");
            ThrowIfFalse(first != notEqualToFirst, "!= should be true when used to compare an object to another reference.");
        }

        private void SymmetricEqualityTest()
        {
            new SymmetricEqualityTestHelper<TValueObject>(_createTestObjectCallback, _createTestObjectCallback).PerformTest();

            new SymmetricEqualityTestHelper<TValueObject>(_createTestObjectCallback, _createUnequalTestObjectCallback).PerformTest();
        }

        public void TransitiveEqualityTest()
        {
            new TransitiveEqualityTestHelper<TValueObject>(_createUnequalTestObjectCallback).PerformTest();
        }

        #endregion

        # region Support Methods

        /// <summary>
        /// Throws an appropriate exception if <paramref name="toEvaluate"/> is false, making the exceptions message 
        /// <paramref name="message"/>.
        /// </summary>
        private void ThrowIfFalse(bool toEvaluate, string message)
        {
            if (toEvaluate == false)
            {
                // TODO: Replace with a custom exception...
                throw new ArgumentException(message);
            }
        }

        /// <summary>
        /// Throws an appropriate exception if <paramref name="toEvaluate"/> is true, making the exceptions message 
        /// <paramref name="message"/>.
        /// </summary>
        private void ThrowIfTrue(bool toEvaluate, string message)
        {
            ThrowIfFalse(!toEvaluate, message);
        }

        # endregion
    }
}
